fix: revert unsafe partial aggregates after final fallback - #5421
Conversation
644e38f to
cfa3113
Compare
|
cc @andygrove @comphead discovered this correctness issue when running TPC-DS benchmark internally with |
|
I used an LLM (Claude Code) to help with this review, including building both PRs locally and running the sweeps below. I have gone over the results myself and I agree with them, but flagging the tooling up front. Thanks for digging into this. The gap you identified is real, and the reasoning about I swept every aggregate function through the Comet-Partial plus Spark-Final configuration, using With this PR applied the decimal case is fixed and Everything else the PR newly blocks was already correct in my sweep. The other thing I noticed is that #5420 already contains So on the evidence I have, the native change in #5420 fixes strictly more than this guard does and costs no native aggregation, while this guard fixes less and gives up native partials for a fair number of aggregates. Could you say what this PR adds once #5420 lands? The description asserts the safeguard is still needed after that, but I was not able to construct a case, and none of the four new tests exercises one. If the value is defense in depth against buffer mismatches we have not found yet, rather than a bug that is broken today, that seems like a reasonable position to me, but I think it should be argued on that basis and weighed against the measured cost. It also affects the ordering of the two PRs. Two smaller things while I was in there. One last note, unrelated to this PR. The sweep also turned up that |
Why are the changes needed?
Closes #5419.
Comet can silently produce incorrect results when an aggregate starts in Comet but finishes in Spark after its shuffle falls back. Although the planner already tries to reject unsafe mixed-engine aggregation, its existing check runs before child operators are converted. A final aggregate can therefore appear convertible early in planning but later remain in Spark because its shuffle child is not native. Its partial aggregate has already been converted to Comet, leaving an execution boundary that the aggregate's intermediate buffer was never declared safe to cross.
For example, consider eight rows spread across four Parquet files, each with
amount = CAST(200 AS DECIMAL(20, 2)). With native shuffle disabled, run:Only one partition contains the matching row. Spark correctly returns
200.000000, but Comet returnsNULLwith adaptive execution either enabled or disabled. The problematic plan is:Decimal
AVGis explicitly ineligible for mixed Spark/Comet execution because its intermediate state has compatibility requirements beyond simply matching the output data type. In this example, an empty Comet partial contributes a null sum that poisons Spark's final aggregation. The same admission gap can affect other aggregates whose buffers are not declared safe for mixed execution, including distinct aggregations with intermediate merge stages. With AQE enabled, the plan must be repaired before a shuffle stage materializes; afterward, its incompatible buffers have already been produced.What changes were proposed in this PR?
Base the fallback decision on the execution plan that conversion actually produced, not solely on an early estimate of whether aggregate expressions look convertible. The existing pre-conversion check remains useful, but a new post-conversion reconciliation verifies that any aggregate left on Spark is not consuming an unsupported Comet partial. When such a boundary exists, its feeding partial aggregate and associated aggregation/shuffle chain are restored to their Spark implementations before execution begins.
The corrected plan keeps the incompatible aggregate buffer inside Spark while preserving native work below it:
This fallback is deliberately local rather than a blanket retreat to Spark. Distinct-aggregate chains and AQE stage replanning remain consistent, while existing materialized or reused stages are not rewritten. Native scans, filters, and projections below the aggregate stay native; supported mixed aggregates such as
MINandMAX, as well as aggregate pipelines that can run entirely in Comet, retain their existing native execution.This PR changes neither the mixed-execution eligibility policy nor native accumulator behavior. The separate empty-partial
AVGbuffer defect is tracked in #5418 and fixed by #5420. Correcting that representation does not eliminate the need for this planner safeguard: decimalAVGand other unsupported aggregate buffers still cannot be freely exchanged between Spark and Comet.How was this PR tested?
All four new targeted regressions fail on public
mainand pass after this change. They cover the decimalAVGwrong-result case with AQE disabled and enabled, along with ordinary and distinct aggregate chains when native shuffle cannot be used.The complete
CometAggregateSuite,CometExecRuleSuite, andCometShuffleFallbackStickinessSuitepassed on Spark 4.0.4 / Java 17: 121 tests passed, with no failures or aborted suites and two existing Spark-version-gated cancellations. The public-base native library was unchanged. Plan assertions verify AQE materialization, repeated whole-plan and stage-only planning, preserved native operators below the partial, safe mixedMIN/MAX, and fully native aggregate chains.An independent standalone-JAR replay against stock Spark 4.0.2 confirmed the corrected decimal
AVGresult with AQE on and off. The separate integer/narrow-decimal empty-partition defect from #5418 remains reproducible when this planner fix is tested alone; applying both fixes together matches Spark in 20/20 synthetic cases. Root-reactor Maven packaging, Spotless, Scala style checks, andgit diff --checkalso passed. GitHub CI reports 63 passing checks, with nine inapplicable checks skipped.